package cxm.proj.xtalk.smack;

import java.util.Arrays;

import android.content.Context;
import android.media.AudioFormat;
import android.media.AudioManager;
import android.media.AudioRecord;
import android.media.AudioTrack;
import android.media.MediaRecorder;
import android.util.Log;

import com.android.webrtc.audio.MobileAEC;
import com.android.webrtc.audio.NoiseSuppression;
import com.android.webrtc.audio.VoiceActivityDetector;

import cxm.lib.speex.EchoCancellation;
import cxm.lib.speex.Preprocessor;
import cxm.lib.util.MyApplication;
import cxm.lib.util.MyLog;
import cxm.lib.util.XArrays;

public class RecordTask implements Runnable {
	public interface RecordTaskDataTransfer {
		public void onRecordTaskDataReceived(byte []dataArray);
	}
	
	private Thread thread = null;
	short []buffer = null;
	short []buffer2 = null;
	short []buffer3 = null;
	short []bufferReceived = null;
	byte []sendBuffer = null;
	
	// params for recording and playing
	protected int freq = 8000;
	protected int audioFormat = AudioFormat.ENCODING_PCM_16BIT;
	protected int channelIn = AudioFormat.CHANNEL_IN_MONO;
	protected int channelOut = AudioFormat.CHANNEL_OUT_MONO;
	
	// for record / playback handle
	private AudioRecord record = null;
	private AudioTrack track = null;
	private MobileAEC aecm = null;
	private NoiseSuppression ns = null;
	private VoiceActivityDetector vad = null;
	private RecordTaskDataTransfer transfer = null;
	private EchoCancellation speexEC = null;
	private Preprocessor speexPro = null;
	
	private boolean isRecording = false;

	private int minJitter = 0;
	private int maxJitter = 0;
	private long t_render = 0;
	private long t_analyze = 0;
	private long t_process = 0;
	private long t_capture = 0;
	private int recordCount = 0;
	private int playCount = 0;

	public RecordTask(RecordTaskDataTransfer transfer, int len) throws Exception {
		// buffer for AudioRecord
		this.buffer = new short[len / 2];
		this.buffer2 = new short[len / 2];
		this.buffer3 = new short[len / 2];
		this.sendBuffer = new byte[len];
		this.bufferReceived = new short[len / 2];
		this.transfer = transfer;
		
		try {
			// new speex for echo cancellation
			this.aecm = new MobileAEC(MobileAEC.SamplingFrequency.FS_8000Hz);
			this.aecm.setAecmMode(MobileAEC.AggressiveMode.AGGRESSIVE).prepare();
			this.ns = new NoiseSuppression(NoiseSuppression.NS_MILD, this.freq);
			this.vad = new VoiceActivityDetector();
			this.vad.setMode(VoiceActivityDetector.VAD_SUPERHIGH);
			this.speexEC = new EchoCancellation(this.buffer.length, 160 * 10); // 100ms buffer
			this.speexPro = new Preprocessor(this.buffer.length, this.freq);
			this.speexPro.enableEchoCancel(this.speexEC);
			this.speexPro.setNoiseSuppressLevel(-12);
			this.speexPro.enableGain(true);
			this.speexPro.setGainLevel(12000);
		} catch (Exception e) {
			MyLog.Log(MyLog.Level.Error, "Cannot create effecitive: " + e);
			this.release();
			throw e;
		}
		
		// new audio record
		this.minJitter = AudioRecord.getMinBufferSize(this.freq,
				this.channelIn, this.audioFormat);
		this.minJitter = this.minJitter > len ? this.minJitter : len;
		this.record = new AudioRecord(MediaRecorder.AudioSource.MIC,
				this.freq, this.channelIn, this.audioFormat, this.minJitter);
		this.maxJitter = AudioTrack.getMinBufferSize(freq,
				this.channelOut, this.audioFormat) * 2;
		this.maxJitter = this.maxJitter > len ? this.maxJitter : len;
		this.track = new AudioTrack(AudioManager.STREAM_MUSIC, this.freq,
				this.channelOut, this.audioFormat, maxJitter,
				AudioTrack.MODE_STREAM);
	}
	
	public void start() {
		if (null != this.thread)
			throw new IllegalStateException("Thread start");
		
		// set mic volume to half
		AudioManager am = (AudioManager)MyApplication.context.getSystemService(Context.AUDIO_SERVICE);
		int volume = am.getStreamMaxVolume(AudioManager.STREAM_VOICE_CALL);
		am.setStreamVolume(AudioManager.STREAM_VOICE_CALL, volume, 0);
		MyLog.Log(MyLog.Level.Debug, String.format("Max volume %d and set to %d", volume, volume));

		// create Thread
		this.thread = new Thread(this);
		this.track.play();
		this.record.startRecording();
		
		// start thread
		this.isRecording = true;
		this.thread.start();
	}

	public void stop() {
		if (null == this.thread)
			return;
		
		// stop record
		record.stop();
		track.stop();
		
		// stop thread
		this.isRecording = false;
		try {
			MyLog.Log(MyLog.Level.Debug, "Before join thread " + this.thread);
			this.thread.join();
			this.thread = null;
		} catch (Exception e) {
			MyLog.Log(MyLog.Level.Error, "Cannot stop thread " + e);
		}
	}
	
	public void release() {
		if (null != aecm)
			aecm.close();
		if (null != ns)
			ns.close();
		if (null != vad)
			vad.close();
		if (null != this.speexEC)
			this.speexEC.close();
		if (null != this.speexPro)
			this.speexPro.close();
		record.release();
		track.release();
		record = null;
		track = null;
	}

	@Override
	public void run() {
		// set thread priority
		android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_URGENT_AUDIO);

		// process streams
		while (isRecording) {
			int bufferReadResult = record.read(buffer, 0, buffer.length);
			if (bufferReadResult == buffer.length) {
				this.sendRecordStream(buffer, buffer2, buffer3);
				this.recordCount++;
			}
			else
				Log.e("MainActivity", "Read buffer size unmatch");
		}
	}
	
	/*
    void changeVolume(short []buffer) {
    	float factor = 4f;
    	float gain = 10.0f;
		int tmpVolume = 0;
    	for (int i = 0; i < buffer.length; i++) {
    		if (buffer[i] < gain) {
    			buffer[i] = 0;
    			continue;
    		}

    		tmpVolume = (int)(buffer[i] * factor);
    		if (tmpVolume > 65535)
    			buffer[i] = (short) 65535;
    		else
    			buffer[i] = (short)tmpVolume;
    	}
    }
    */
	
	public void receiveStreamToPlay(byte []payload) {
    	// TODO write to audio receiver
    	if (payload.length != this.buffer.length * 2 || null == track) {
    		MyLog.Log(MyLog.Level.Error, "Receive buffer size not match");
    		return;
    	}

    	// translate to short array
    	XArrays.byteArray2ShortArray(payload, this.bufferReceived);
    	
    	// try to increate volume
    	// this.changeVolume(shortBuffer);

    	// recording first time before render buffer
    	this.t_analyze = System.currentTimeMillis();
    	try {
    		// speex
    		this.speexEC.playback(this.bufferReceived);

    		// webrtc
    		aecm.farendBuffer(this.bufferReceived, (short)this.bufferReceived.length);
    	} catch (Exception e) {
    		MyLog.Log(MyLog.Level.Error, "Error for process far end buffer");
    		return;
    	}

    	// write to buffer
    	track.write(this.bufferReceived, 0, this.bufferReceived.length);
    	// record last time for render buffer
    	this.t_render = System.currentTimeMillis();
    	
    	// record the count
    	this.playCount++;
	}
	
	private void sendRecordStream(short []payload, short []processBuffer, short []ecBuffer) {
		if (payload.length != this.buffer.length) {
			MyLog.Log(MyLog.Level.Error, "Send buffer size not match");
			return;
		}
		
		// farend buffer
		try {
			// recording first time buffer is captured
			this.t_capture = System.currentTimeMillis();

			// speex first
			this.speexPro.run(payload);
			this.speexEC.capture(payload, processBuffer);
			// nose suppression
			this.ns.process(processBuffer, payload);
			// recording the time pass to echo cancellation
			this.t_process = System.currentTimeMillis();

			// calculate delay
			short delay = (short)((this.t_render - this.t_analyze) + 140 + // try to add 100ms for speex
					(this.t_process - this.t_capture) + (maxJitter + minJitter) / 160);
			// cancel echo
			aecm.echoCancellation(processBuffer, payload, ecBuffer, (short)payload.length, delay);
			// aecm.echoCancellation(payload, processBuffer, ecBuffer, (short)payload.length, delay);

			if (!vad.process(freq, ecBuffer)) 
				Arrays.fill(ecBuffer, (short)0);
			/*
			else
				MyLog.Log(MyLog.Level.Debug, String.format("Total delay %d with rec/play %d, hard %d, soft %d",
						delay, (maxJitter + minJitter) / 80, (this.t_render - this.t_analyze),
						(this.t_process - this.t_capture)));
			*/

			XArrays.shortArray2ByteArray(ecBuffer, this.sendBuffer);
			// byte []sendBuffer = XArrays.shortArray2ByteArray(nsBuffer);
			transfer.onRecordTaskDataReceived(sendBuffer);
		} catch (Exception e) {
			Log.e("MainActivity", "process echo cancel failed: " + e.toString());
			Log.e("MainActivity", "With buffer length " + payload.length);
		}
	}
	
	public int getRecordCount() {
		return this.recordCount;
	}
	
	public int getPlayCount() {
		return this.playCount;
	}
}
